using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

namespace csammisrun.OscarLib.Utility
{
  public delegate void DataReaderCompleteHandler();

  /// <summary>
  /// Describes the flags that can accompany a Direct IM
  /// </summary>
  enum DirectIMFlags
  {
    /// <summary>
    /// The DirectIM packet is an ordinary message
    /// </summary>
    None = 0x00,
    /// <summary>
    /// The message being sent is an automatic response (away message)
    /// </summary>
    AutoResponse = 0x01,
    /// <summary>
    /// The message being sent indicates a change in typing status
    /// </summary>
    /// <remarks>Sent alone, this flag indicates the typing has ceased. Sent with either <see cref="UserTyping"/> or <see cref="UserTyped"/>,
    /// the semantics of the packet are as described in those flags' respective comments.</remarks>
    TypingPacket = 0x02,
    /// <summary>
    /// The remote user is typing
    /// </summary>
    UserTyping = 0x08,
    /// <summary>
    /// The remote user has paused in their typing
    /// </summary>
    UserTyped = 0x04,
    /// <summary>
    /// This packet is to confirm the successful negotiation of a direct connection
    /// </summary>
    ConfirmationPacket = 0x20
  }

  class DirectIMConnection : DirectConnection
  {

    public DirectIMConnection(Session parent, int id, DirectConnectionMethod method, DirectConnectRole role)
      : base(parent, id, method, role)
    {
    }

    public override void ReadHeader()
    {
      try
      {
        byte[] odcheader = new byte[6];
        _sock.BeginReceive(odcheader, 0, odcheader.Length, SocketFlags.None, new AsyncCallback(EndReadHeader), odcheader);
      }
      catch (Exception ex)
      {
        DisconnectFromServer(true);
      }
    }

    private void EndReadHeader(IAsyncResult res)
    {
      byte[] odcheader = null;
      try
      {
        odcheader = (byte[])res.AsyncState;
        _sock.EndReceive(res);
      }
      catch (Exception ex)
      {
        DisconnectFromServer(true);
      }

      // Verify that this is an ODC header
      if (Encoding.ASCII.GetString(odcheader, 0, 4) != "ODC2")
      {
        // Huh...
        return;
      }

      ushort datalen = Marshal.ByteArrayToUshort(odcheader, 4);
      if (datalen < 6)
      {
        // Oh
        return;
      }

      byte[] messageheader = ReadPacket(datalen - 6);
      if (messageheader == null)
      {
        return;
      }

      // Extract various members from the message header
      byte[] cookie = new byte[8];
      uint datalength = 0;
      ushort charset = 0;
      ushort subcharset = 0;
      DirectIMFlags flags = DirectIMFlags.None;
      string screenname = "";

      Array.ConstrainedCopy(messageheader, 6, cookie, 0, 8);
      datalength = Marshal.ByteArrayToUint(messageheader, 22);
      charset = Marshal.ByteArrayToUshort(messageheader, 26);
      subcharset = Marshal.ByteArrayToUshort(messageheader, 28);
      flags = (DirectIMFlags)Marshal.ByteArrayToUint(messageheader, 30);
      screenname = Encoding.ASCII.GetString(messageheader, 38, 16);

      if ((flags & DirectIMFlags.TypingPacket) != 0)
      {
        // Determine the type of typing packet this is
        if ((flags & DirectIMFlags.UserTyping) != 0)
        {
          _parent.OnTypingNotification(screenname, TypingNotification.TypingStarted);
        }
        else if ((flags & DirectIMFlags.UserTyped) != 0)
        {
          _parent.OnTypingNotification(screenname, TypingNotification.TextTyped);
        }
        else
        {
          _parent.OnTypingNotification(screenname, TypingNotification.TypingFinished);
        }
        // Probably no data, but read it in anyway
        ReadPacket((int)datalength);
        ReadHeader();
        return;
      }
      
      if ((flags & DirectIMFlags.ConfirmationPacket) != 0)
      {
        // Do we really do anything here?  I don't think so.
        ReadPacket((int)datalength);
        ReadHeader();
        return;
      }
      
      // Create a new instant message
      DirectIM dim = new DirectIM(screenname, this);
      dim.Cookie = cookie;
      dim.IsAutoResponse = ((flags & DirectIMFlags.AutoResponse) != 0);
      dim.Encoding = IM.GetEncodingFromCharset(charset, subcharset);

      DirectIMDataReader reader = new DirectIMDataReader(this, dim);
      reader.DataReaderComplete += new DataReaderCompleteHandler(delegate
      {
        _parent.OnDirectIMReceived(reader.Message);
        ReadHeader();
      });
      reader.Read(datalength);
    }
  }

  /// <summary>
  /// Encapsulates the logic to read a message from a DirectIMConnection stream
  /// </summary>
  class DirectIMDataReader : IDisposable
  {
    private const int BUFFERSIZE = 1024;
    private DirectIMConnection _conn = null;
    private DirectIM _msg = null;
    private byte[] _fullbuffer = null;
    private int _offset = 0;

    private const string DATATAG = @"<data \s+? id=[""]?(?<id>\w+)[""]? \s+? size=[""]?(?<size>\w+)[""]? \s+? >(?<data>.*?)</data>";
    private Regex _datamatch = new Regex(DATATAG, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline);

    /// <summary>
    /// Signals that the DataReader has finished processing the message
    /// </summary>
    public event DataReaderCompleteHandler DataReaderComplete;
    
    /// <summary>
    /// Initializes a new DirectIMDataReader
    /// </summary>
    public DirectIMDataReader(DirectIMConnection connection, DirectIM message)
    {
      _conn = connection;
      _conn.ReadPacketAsyncComplete += new ConnectionReadPacketHandler(_conn_ReadPacketAsyncComplete);
      _msg = message;
    }

    #region Methods
    /// <summary>
    /// Begins the data reading process
    /// </summary>
    public void Read(uint datasize)
    {
      _fullbuffer = new byte[datasize];

      // Read in little chunks until we find ourselves done
      _conn.ReadPacketAsync((int)Math.Min(BUFFERSIZE, datasize));
    }

    /// <summary>
    /// Append a chunk of data onto the full message buffer
    /// </summary>
    private void _conn_ReadPacketAsyncComplete(Connection conn, byte[] data)
    {
      Array.ConstrainedCopy(data, 0, _fullbuffer, _offset, data.Length);
      _offset += data.Length;
      data = null;

      if (_offset < _fullbuffer.Length)
      {
        _conn.ReadPacketAsync(Math.Min(BUFFERSIZE, _fullbuffer.Length - _offset));
        return;
      }

      ProcessBuffer();
    }

    /// <summary>
    /// Process the contents of the raw message into the DirectIM object
    /// </summary>
    private void ProcessBuffer()
    {
      int index = 0;
      int length = _fullbuffer.Length;
      
      if ((index = IndexOfInBuffer("<BINARY>", _fullbuffer)) == -1)
      {
        index = length;
      }

      // Append the data up to the <binary> tag, if one was found, onto the IM text
      byte[] pretag = new byte[index];
      Array.ConstrainedCopy(_fullbuffer, 0, pretag, 0, pretag.Length);
      _msg.Message += _msg.Encoding.GetString(pretag);
      pretag = null;

      // Convert the rest of the string to ASCII
      string data = Encoding.ASCII.GetString(_fullbuffer, index, _fullbuffer.Length - index);
      _fullbuffer = null;

      foreach (Match m in _datamatch.Matches(data))
      {
        int id = Int32.Parse(m.Groups["id"].Value);
        byte[] payload = Encoding.ASCII.GetBytes(m.Groups["data"].Value);
        _msg.Attachments.Add(new Attachment(id, payload));
      }

      if (DataReaderComplete != null)
      {
        DataReaderComplete();
      }
    }

    /// <summary>
    /// Finds an ASCII string occurance in a byte buffer
    /// </summary>
    private int IndexOfInBuffer(string needle, byte[] haystack)
    {
      int i = 0, j = 0;
      for (i = 0; i < haystack.Length - needle.Length; i++)
      {
        for (j = 0; j < needle.Length; j++)
        {
          if (needle[j] != (char)haystack[i + j])
          {
            break;
          }
        }

        if (j == needle.Length)
        {
          return i;
        }
      }
      return -1;
    }
    #endregion

    #region Properties
    /// <summary>
    /// Gets the <see cref="DirectIM"/> being populated by the data reader
    /// </summary>
    public DirectIM Message
    {
      get { return _msg; }
    }
    #endregion

    #region IDisposable Members

    public void Dispose()
    {
      _fullbuffer = null;
      _conn.ReadPacketAsyncComplete -= new ConnectionReadPacketHandler(_conn_ReadPacketAsyncComplete);
    }

    #endregion
  }
}
